/* Copyright 2008 Tim Fennell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sourceforge.stripes.validation.expression;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.controller.ParameterName;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.controller.StripesConstants;
import net.sourceforge.stripes.validation.ValidationMetadata;
import net.sourceforge.stripes.validation.ValidationErrors;
import net.sourceforge.stripes.validation.ValidationError;
import net.sourceforge.stripes.validation.ScopedLocalizableError;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.bean.BeanUtil;
import net.sourceforge.stripes.exception.StripesRuntimeException;

import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.ServletContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.PropertyNotWritableException;
import javax.el.FunctionMapper;
import javax.el.VariableMapper;
import javax.el.ELException;
import java.util.List;
import java.util.Iterator;
import java.beans.FeatureDescriptor;
import java.lang.reflect.Method;

/**
 * An implementation of {@link ExpressionExecutor} that uses the new EL API available in Java
 * EE 5 in the {@link javax.el} package. While more complicated that the JSP 2.0 API it has
 * one advantage which is that it can be used without the need to allocate a PageContext
 * object and without any other libraries being available.
 *
 * @author tfenne
 * @since Stripes 1.5
 */
public class Jsp21ExpressionExecutor implements ExpressionExecutor {
    private static final Log log = Log.getInstance(Jsp21ExpressionExecutor.class);

    /**
     * Implementation of the EL interface to resolve variables. Resolves variables by
     * checking two special names ("this" and "actionBean") and then falling back to
     * retrieving property values from the ActionBean passed in to the constructor.
     *
     * @author Tim Fennell
     * @since Stripes 1.5
     */
    protected static class StripesELResolver extends ELResolver {
        private ActionBean bean;
        private Object currentValue;

        /** Constructs a resolver based on the action bean . */
        StripesELResolver(ActionBean bean) {
            this.bean = bean;
        }

        /** Sets the value that the 'this' variable will point at. */
        void setCurrentValue(Object value) {
            this.currentValue = value;
        }

        /**
         * Attempts to resolve the value as described in the class level javadoc.
         * @param ctx the ELContext for the expression
         * @param base the object on which the property resides (null == root property)
         * @param prop the name of the property being looked for
         * @return the value of the property or null if one can't be found
         */
        @Override
        public Object getValue(ELContext ctx, Object base, Object prop) {
            if ("this".equals(prop)) {
                ctx.setPropertyResolved(true);
                return this.currentValue;
            }
            else if (StripesConstants.REQ_ATTR_ACTION_BEAN.equals(prop)) {
                ctx.setPropertyResolved(true);
                return this.bean;
            }
            else {
                try {
                    base = base == null ? this.bean : base;
                    Object retval = BeanUtil.getPropertyValue(String.valueOf(prop), base);
                    ctx.setPropertyResolved(true);
                    return retval;
                }
                catch (Exception e) { return null; }
            }
        }

        /** Does nothing. Always returns Object.class. */
        @Override
        public Class<?> getType(final ELContext ctx, final Object base, final Object prop) {
            ctx.setPropertyResolved(true);
            return Object.class;
        }

        /** Does nothing. Always throws PropertyNotWritableException. */
        @Override
        public void setValue(ELContext elContext, Object o, Object o1, Object o2) throws PropertyNotWritableException {
            throw new PropertyNotWritableException("Unsupported Op");
        }

        /** Always returns true. */
        @Override
        public boolean isReadOnly(final ELContext elContext, final Object o, final Object o1) { return true; }

        /** Always returns null. */
        @Override
        public Iterator<FeatureDescriptor> getFeatureDescriptors(final ELContext elContext, final Object o) { return null; }

        /** Always returns Object.class. */
        @Override
        public Class<?> getCommonPropertyType(final ELContext elContext, final Object o) { return Object.class; }
    }

    /**
     * Implementation of the EL interface for managing expression context. Resolves variables
     * using the StripesELResolver above.  Both the FunctionMapper and VariableResolver are
     * essentially no-op implementations.
     *
     * @author Tim Fennell
     * @since Stripes 1.5
     */
    protected static class StripesELContext extends ELContext {
        @SuppressWarnings("unused")
        private ActionBean bean;
        private StripesELResolver resolver;
        private VariableMapper vmapper;
        private static final FunctionMapper fmapper = new FunctionMapper() {
            @Override
            public Method resolveFunction(final String s, final String s1) { return null; }
        };

        /**
         * Constructs a new instance using the ActionBean provided as the source for most
         * property resolutions.
         *
         * @param bean the ActionBean to resolve properties against
         */
        public StripesELContext(ActionBean bean) {
            this.bean = bean;
            this.resolver = new StripesELResolver(bean);

            this.vmapper = new VariableMapper() {
                @Override
                public ValueExpression resolveVariable(final String s) {
                    return null;
                }

                @Override
                public ValueExpression setVariable(final String s, final ValueExpression valueExpression) {
                    return null;
                }
            };
        }

        /** Sets the current value of the 'this' special variable. */
        public void setCurrentValue(final Object value) {resolver.setCurrentValue(value);}

        /** Returns the StripesELResovler. */
        @Override
        public StripesELResolver getELResolver() { return this.resolver; }

        /** Returns a no-op implementation of FunctionMapper. */
        @Override
        public FunctionMapper getFunctionMapper() { return fmapper; }

        /** Returns a no-op implementation of VariableMapper. */
        @Override
        public VariableMapper getVariableMapper() { return vmapper; }
    }

    /** Default constructor that throws an exception if the JSP2.1 APIs are not available. */
    public Jsp21ExpressionExecutor() {
        if (getExpressionFactory() == null) {
            throw new StripesRuntimeException("Could not create a JSP2.1 ExpressionFactory.");
        }
    }

    // See interface for javadoc.  
    public void evaluate(final ActionBean bean, final ParameterName name, final List<Object> values,
                         final ValidationMetadata validationInfo, final ValidationErrors errors) {

        StripesELContext ctx = null;
        String expressionString = validationInfo.expression();
        ValueExpression expression = null;

        try {
            if (expressionString != null) {
                // Make sure we can get an factory
                ExpressionFactory factory = getExpressionFactory();
                if (factory == null) return;

                ctx = new StripesELContext(bean);

                // If this turns out to be slow we could probably cache the parsed expression
                expression = factory.createValueExpression(ctx, expressionString, Boolean.class);
            }
        }
        catch (ELException ele) {
            throw new StripesRuntimeException(
                    "Could not parse the EL expression being used to validate field " +
                    name.getName() + ". This is not a transient error. Please double " +
                    "check the following expression for errors: " +
                    validationInfo.expression(), ele);
        }

        for (Object value : values) {
            // And then if we have an expression to use
            if (expression != null) {
                try {
                    ctx.setCurrentValue(value);
                    Boolean result = (Boolean) expression.getValue(ctx);
                    if (!Boolean.TRUE.equals(result)) {
                        ValidationError error = new ScopedLocalizableError(ERROR_DEFAULT_SCOPE, ERROR_KEY);
                        error.setFieldValue(String.valueOf(value));
                        errors.add(name.getName(), error);
                    }
                }
                catch (ELException ele) {
                    log.error("Error evaluating expression for property ", name.getName(),
                              " of class ", bean.getClass().getSimpleName(), ". Expression: ",
                              validationInfo.expression());
                }
            }
        }
    }

    /** Creates an ExpressionFactory using the JspApplicationContext. */
    protected ExpressionFactory getExpressionFactory() {
        ServletContext ctx = StripesFilter.getConfiguration().getServletContext();
        JspApplicationContext jspCtx = JspFactory.getDefaultFactory().getJspApplicationContext(ctx);
        return jspCtx.getExpressionFactory();
    }
}
